package com.jxpanda.infrastructure.core.tollkit.probability;

import com.jxpanda.infrastructure.core.tollkit.RandomKit;
import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * PRD概率算法的entity
 *
 * @author Panda
 */
@Data
@AllArgsConstructor
@SuppressWarnings("AlibabaClassNamingShouldBeCamel")
public class PRD {

    /**
     * 期望概率
     */
    private final double probability;

    /**
     * 尝试次数N，默认从1开始，每次失败的话递增1
     */
    private volatile int number = 1;

    /**
     * 期望概率下对应的c值
     */
    private final double critical;

    /**
     * 这个构造器会使用期望概率反推出c值来，性能较低
     * 在使用量小的时候可以使用
     */
    public PRD(double probability) {
        this.probability = probability;
        this.critical = calculateCriticalFromProbability(probability);
    }

    /**
     * 这个构造器不计算c值，使用外部传递进来的c值计算
     * 数据量大的时候，这个可以加载c值表
     */
    public PRD(double probability, double critical) {
        this.probability = probability;
        this.critical = critical;
    }

    /**
     * 执行一次暴击判定
     */
    public synchronized boolean hit() {
        boolean isCritical = RandomKit.randomDoubleRange(0, 1) <= actualProbability();
        if (isCritical) {
            this.number = 1;
        } else {
            this.number++;
        }
        return isCritical;
    }

    /**
     * 返回实际概率（公式：critical * ）
     */
    public double actualProbability() {
        return this.getCritical() * this.getNumber();
    }

    /**
     * 计算给定C值下的期望概率是多少
     *
     * @param critical 暴击率
     * @return 期望平均概率
     */
    private static double calculateProbabilityFromCritical(double critical) {
        // 累加的概率
        double probabilityTotal = 0;
        // 最终的平均概率
        double finalProbability = 0;
        // 失败次数的上限，防止陷入无限死循环
        int failLimit = (int) Math.ceil(1 / critical);
        for (int i = 1; i <= failLimit; ++i) {
            // 当前概率
            double currentProbability = Math.min(1D, i * critical) * (1 - probabilityTotal);
            probabilityTotal += currentProbability;
            finalProbability += i * currentProbability;
        }
        return 1 / finalProbability;
    }

    /**
     * 根据传入的期望概率P，计算初始的暴击率C
     * 原理就是二分法不断逼近，在数据量大的时候性能不太好
     * 因此数据量大的时候，需要预结算出C值表，然后使用另外一个构造器直接加载对象
     *
     * @param probability 期望概率P
     * @return 初始暴击率C
     */
    private static double calculateCriticalFromProbability(double probability) {
        // 二分法上界是期望概率
        double up = probability;
        // 二分法下界是0
        double low = 0d;
        // 循环中上一次计算的暴击值
        double lastCritical = 1d;
        while (true) {
            // 求中间值
            double mid = (up + low) / 2d;
            // 计算期望概率
            double testedCritical = calculateProbabilityFromCritical(mid);
            // 如果误差满足需求了，则停止计算
            if (Math.abs(testedCritical - lastCritical) <= 0.00005d) {
                break;
            }
            // 重设上/下界，进行下一次逼近
            if (testedCritical > probability) {
                up = mid;
            } else {
                low = mid;
            }
            lastCritical = testedCritical;
        }
        // 上界在计算过程是不断被降低的，所以计算完成后，上界就是初始暴击率
        return up;
    }
}
